package redis

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/url"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"time"

	"gitee.com/andrewgithub/go-redis/v9/internal/pool"
)

// Limiter is the interface of a rate limiter or a circuit breaker.
type Limiter interface {
	// Allow returns nil if operation is allowed or an error otherwise.
	// If operation is allowed client must ReportResult of the operation
	// whether it is a success or a failure.
	Allow() error
	// ReportResult reports the result of the previously allowed operation.
	// nil indicates a success, non-nil error usually indicates a failure.
	ReportResult(result error)
}

// Options keeps the settings to set up redis connection.
type Options struct {
	// 网络类型，tcp or unix 默认 tcp
	Network string
	// host:port 地址.
	Addr string

	// 每个连接都会执行 CLIENT SETNAME ClientName 命令为每个连接设置客户端名字
	ClientName string

	// Dialer 会创建网络连接，并且有限Network和Addr，也就是说一旦创建Network和Addr设置的网络连接将失效
	Dialer func(ctx context.Context, network, addr string) (net.Conn, error)

	// Hook 当连接建立成功的时候会回调该函数.
	OnConnect func(ctx context.Context, cn *Conn) error

	// Protocol 2 or 3. 用来和redis协商使用哪个协议版本的字段
	// Default is 3.
	Protocol int
	//ACL（Access Control List）：Redis 6.0 引入了 ACL 系统，用于更细粒度地控制客户端对 Redis 服务器的访问权限。
	// Username 字段用于在连接到使用 Redis ACL 系统的 Redis 6.0 或更高版本实例时，指定用于身份验证的用户名。
	Username string
	// Redis ACL系统支持通过密码认证，该字段就是密码
	Password string
	// CredentialsProvider 允许更改用户名和密码，当更新之前这里返回原先的用户名和密码
	CredentialsProvider func() (username string, password string)

	// CredentialsProviderContext 是 CredentialsProvider 的增强版本,
	// CredentialsProviderContext 存在会忽略 CredentialsProvider，后期会合并两个接口只保留一个
	CredentialsProviderContext func(ctx context.Context) (username string, password string, err error)

	// 选择哪个数据库，下支持0-15
	DB int

	// 尝试次数，默认是3次，-1 (not 0)关闭重试，只执行一次
	// 因为init接口里面会重新修改值，当为-1时
	MaxRetries int

	// 每次重试之间的最小重试间隔。默认值为8毫秒；-1表示禁用重试间隔
	MinRetryBackoff time.Duration
	// 每次重试之间最大时间间隔，默认为512毫秒，-1表示禁用重试间隔
	MaxRetryBackoff time.Duration

	// 拨号超时时间 默认是5秒
	DialTimeout time.Duration
	// 同步等待回复超时时间，如果超时命令执行失败
	//   - `0` - 默认 (3 seconds).
	//   - `-1` - 阻塞等待 (block indefinitely).
	//   - `-2` - 完全禁用SetReadDeadline调用
	ReadTimeout time.Duration
	// 写超时时间
	//   - `0` - 默认 (3 seconds).
	//   - `-1` - 阻塞等待 (block indefinitely).
	//   - `-2` - 完全禁止SetWriteDeadline调用
	WriteTimeout time.Duration
	// ContextTimeoutEnabled 为true的情况下会尊重Context上下文的超时时间，否则会忽略.
	// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts
	ContextTimeoutEnabled bool

	// 连接池类型
	// true 是 FIFO 连接池, false 代表 LIFO 连接池.
	// 请注意，FIFO的开销比LIFO略高，
	// 但它有助于更快地关闭空闲连接，从而减小池的大小。
	PoolFIFO bool
	// 连接池中基础套接字连接数量
	// 默认情况下每个可用的CPU核心会有10个连接 runtime.GOMAXPROCS.
	// 当连接池中被耗尽时，客户端会被分配额外的连接
	// 当然你可以使用MaxActiveConns限制连接池大小。
	PoolSize int
	// 表示当所有连接都忙时，客户端从连接池中获取连接的超时时间默认ReadTimeout + 1 为 6 秒。
	// 如果所有连接都在忙，并且客户端在 6 秒内无法获取到连接，则会返回一个错误
	PoolTimeout time.Duration
	// 连接池中最小空闲连接数量
	// Default is 0. 空闲连接默认不会被关闭.
	MinIdleConns int
	// 连接池中最大空闲连接数量
	// Default is 0. 空闲连接默认不会被关闭.
	MaxIdleConns int
	// 最大活跃连接数量
	// 0表示不设限制
	MaxActiveConns int
	// ConnMaxIdleTime 一个连接最长空闲时间.
	// 最后比系统超时时间少，否则将不起作用.
	//过期的连接可能会在重新使用之前被懒惰地关闭。如果d小于或等于0，则由于连接处于空闲状态，不会关闭连接。
	//默认值为30分钟。“-1”禁用空闲超时检查。
	ConnMaxIdleTime time.Duration
	// ConnMaxLifetime是一个连接可以被重用的最大时间。
	//
	// 过期的连接可能会在重用之前惰性关闭。
	// 如果<= 0，连接不会因为连接的"超期"(age)而关闭。
	//
	// 默认不关闭空闲连接。
	ConnMaxLifetime time.Duration

	// 使用的TLS配置。设置后，TLS将进行协商。
	TLSConfig *tls.Config

	// 限制器接口，用于实现断路器或速率限制器。
	Limiter Limiter

	// 在备机 slave/follower 节点上使能只读模式化.
	readOnly bool

	// 是否禁用客户端设置标识符，默认false.
	DisableIndentity bool

	// 为客户端名字添加后缀，默认空.
	IdentitySuffix string

	// EnableUnstable 字段用于启用 Redis Search 模块的不稳定模式（Unstable mode），
	// 并且该模式使用 RESP3 协议.
	UnstableResp3 bool
}

func (opt *Options) init() {
	if opt.Addr == "" {
		// 如果没有配置就采用默认值
		opt.Addr = "localhost:6379"
	}

	if opt.Network == "" {
		// 没有指定的情况下，按照Addr的格式来确定网络类型，unix类型的需要使用/开头 /path/to/redis.sock
		if strings.HasPrefix(opt.Addr, "/") {
			opt.Network = "unix"
		} else {
			opt.Network = "tcp"
		}
	}
	// 没有配置拨号超时时间的情况下，将使用默认值5秒
	if opt.DialTimeout == 0 {
		opt.DialTimeout = 5 * time.Second
	}
	// 用户没有自定义拨号函数的情况下，就使用默认的拨号函数
	if opt.Dialer == nil {
		opt.Dialer = NewDialer(opt)
	}
	// 默认连接池大小为10*CPU核心数
	if opt.PoolSize == 0 {
		opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
	}
	switch opt.ReadTimeout {
	case -2:
		opt.ReadTimeout = -1
	case -1:
		opt.ReadTimeout = 0
	case 0:
		opt.ReadTimeout = 3 * time.Second
	}
	switch opt.WriteTimeout {
	case -2: // 完全禁止SetWriteDeadline调用
		opt.WriteTimeout = -1
	case -1: // 阻塞调用
		opt.WriteTimeout = 0
	case 0:
		opt.WriteTimeout = opt.ReadTimeout
	}
	// 默认opt.ReadTimeout + time.Second，
	// 如果opt.ReadTimeout为0说明是阻塞调用，阻塞的情况下将opt.PoolTimeout设置为30s
	if opt.PoolTimeout == 0 {
		if opt.ReadTimeout > 0 {
			opt.PoolTimeout = opt.ReadTimeout + time.Second
		} else {
			opt.PoolTimeout = 30 * time.Second
		}
	}
	// 默认30分钟
	if opt.ConnMaxIdleTime == 0 {
		opt.ConnMaxIdleTime = 30 * time.Minute
	}
	//
	if opt.MaxRetries == -1 {
		opt.MaxRetries = 0
	} else if opt.MaxRetries == 0 {
		opt.MaxRetries = 3
	}
	switch opt.MinRetryBackoff {
	case -1:
		opt.MinRetryBackoff = 0
	case 0:
		opt.MinRetryBackoff = 8 * time.Millisecond
	}
	switch opt.MaxRetryBackoff {
	case -1:
		opt.MaxRetryBackoff = 0
	case 0:
		opt.MaxRetryBackoff = 512 * time.Millisecond
	}
}

func (opt *Options) clone() *Options {
	clone := *opt
	return &clone
}

// NewDialer returns a function that will be used as the default dialer
// when none is specified in Options.Dialer.
func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) {
	return func(ctx context.Context, network, addr string) (net.Conn, error) {
		netDialer := &net.Dialer{
			Timeout:   opt.DialTimeout,
			KeepAlive: 5 * time.Minute,
		}
		// 不支持tls直接直接进行context拨号
		if opt.TLSConfig == nil {
			return netDialer.DialContext(ctx, network, addr)
		}
		return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
	}
}

// ParseURL parses a URL into Options that can be used to connect to Redis.
// Scheme is required.
// There are two connection types: by tcp socket and by unix socket.
// Tcp connection:
//
//	redis://<user>:<password>@<host>:<port>/<db_number>
//
// Unix connection:
//
//	unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
//
// Most Option fields can be set using query parameters, with the following restrictions:
//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
//   - only scalar type fields are supported (bool, int, time.Duration)
//   - for time.Duration fields, values must be a valid input for time.ParseDuration();
//     additionally a plain integer as value (i.e. without unit) is interpreted as seconds
//   - to disable a duration field, use value less than or equal to 0; to use the default
//     value, leave the value blank or remove the parameter
//   - only the last value is interpreted if a parameter is given multiple times
//   - fields "network", "addr", "username" and "password" can only be set using other
//     URL attributes (scheme, host, userinfo, resp.), query parameters using these
//     names will be treated as unknown parameters
//   - unknown parameter names will result in an error
//
// Examples:
//
//	redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
//	is equivalent to:
//	&Options{
//		Network:     "tcp",
//		Addr:        "localhost:6789",
//		DB:          1,               // path "/3" was overridden by "&db=1"
//		DialTimeout: 3 * time.Second, // no time unit = seconds
//		ReadTimeout: 6 * time.Second,
//		MaxRetries:  2,
//	}
func ParseURL(redisURL string) (*Options, error) {
	u, err := url.Parse(redisURL)
	if err != nil {
		return nil, err
	}

	switch u.Scheme {
	case "redis", "rediss":
		return setupTCPConn(u)
	case "unix":
		return setupUnixConn(u)
	default:
		return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
	}
}

func setupTCPConn(u *url.URL) (*Options, error) {
	o := &Options{Network: "tcp"}

	o.Username, o.Password = getUserPassword(u)

	h, p := getHostPortWithDefaults(u)
	o.Addr = net.JoinHostPort(h, p)

	f := strings.FieldsFunc(u.Path, func(r rune) bool {
		return r == '/'
	})
	switch len(f) {
	case 0:
		o.DB = 0
	case 1:
		var err error
		if o.DB, err = strconv.Atoi(f[0]); err != nil {
			return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
		}
	default:
		return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path)
	}

	if u.Scheme == "rediss" {
		o.TLSConfig = &tls.Config{
			ServerName: h,
			MinVersion: tls.VersionTLS12,
		}
	}

	return setupConnParams(u, o)
}

// getHostPortWithDefaults is a helper function that splits the url into
// a host and a port. If the host is missing, it defaults to localhost
// and if the port is missing, it defaults to 6379.
func getHostPortWithDefaults(u *url.URL) (string, string) {
	host, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		host = u.Host
	}
	if host == "" {
		host = "localhost"
	}
	if port == "" {
		port = "6379"
	}
	return host, port
}

func setupUnixConn(u *url.URL) (*Options, error) {
	o := &Options{
		Network: "unix",
	}

	if strings.TrimSpace(u.Path) == "" { // path is required with unix connection
		return nil, errors.New("redis: empty unix socket path")
	}
	o.Addr = u.Path
	o.Username, o.Password = getUserPassword(u)
	return setupConnParams(u, o)
}

type queryOptions struct {
	q   url.Values
	err error
}

func (o *queryOptions) has(name string) bool {
	return len(o.q[name]) > 0
}

func (o *queryOptions) string(name string) string {
	vs := o.q[name]
	if len(vs) == 0 {
		return ""
	}
	delete(o.q, name) // enable detection of unknown parameters
	return vs[len(vs)-1]
}

func (o *queryOptions) strings(name string) []string {
	vs := o.q[name]
	delete(o.q, name)
	return vs
}

func (o *queryOptions) int(name string) int {
	s := o.string(name)
	if s == "" {
		return 0
	}
	i, err := strconv.Atoi(s)
	if err == nil {
		return i
	}
	if o.err == nil {
		o.err = fmt.Errorf("redis: invalid %s number: %s", name, err)
	}
	return 0
}

func (o *queryOptions) duration(name string) time.Duration {
	s := o.string(name)
	if s == "" {
		return 0
	}
	// try plain number first
	if i, err := strconv.Atoi(s); err == nil {
		if i <= 0 {
			// disable timeouts
			return -1
		}
		return time.Duration(i) * time.Second
	}
	dur, err := time.ParseDuration(s)
	if err == nil {
		return dur
	}
	if o.err == nil {
		o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err)
	}
	return 0
}

func (o *queryOptions) bool(name string) bool {
	switch s := o.string(name); s {
	case "true", "1":
		return true
	case "false", "0", "":
		return false
	default:
		if o.err == nil {
			o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s)
		}
		return false
	}
}

func (o *queryOptions) remaining() []string {
	if len(o.q) == 0 {
		return nil
	}
	keys := make([]string, 0, len(o.q))
	for k := range o.q {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

// setupConnParams converts query parameters in u to option value in o.
func setupConnParams(u *url.URL, o *Options) (*Options, error) {
	q := queryOptions{q: u.Query()}

	// compat: a future major release may use q.int("db")
	if tmp := q.string("db"); tmp != "" {
		db, err := strconv.Atoi(tmp)
		if err != nil {
			return nil, fmt.Errorf("redis: invalid database number: %w", err)
		}
		o.DB = db
	}

	o.Protocol = q.int("protocol")
	o.ClientName = q.string("client_name")
	o.MaxRetries = q.int("max_retries")
	o.MinRetryBackoff = q.duration("min_retry_backoff")
	o.MaxRetryBackoff = q.duration("max_retry_backoff")
	o.DialTimeout = q.duration("dial_timeout")
	o.ReadTimeout = q.duration("read_timeout")
	o.WriteTimeout = q.duration("write_timeout")
	o.PoolFIFO = q.bool("pool_fifo")
	o.PoolSize = q.int("pool_size")
	o.PoolTimeout = q.duration("pool_timeout")
	o.MinIdleConns = q.int("min_idle_conns")
	o.MaxIdleConns = q.int("max_idle_conns")
	o.MaxActiveConns = q.int("max_active_conns")
	if q.has("conn_max_idle_time") {
		o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
	} else {
		o.ConnMaxIdleTime = q.duration("idle_timeout")
	}
	if q.has("conn_max_lifetime") {
		o.ConnMaxLifetime = q.duration("conn_max_lifetime")
	} else {
		o.ConnMaxLifetime = q.duration("max_conn_age")
	}
	if q.err != nil {
		return nil, q.err
	}

	// any parameters left?
	if r := q.remaining(); len(r) > 0 {
		return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
	}

	return o, nil
}

func getUserPassword(u *url.URL) (string, string) {
	var user, password string
	if u.User != nil {
		user = u.User.Username()
		if p, ok := u.User.Password(); ok {
			password = p
		}
	}
	return user, password
}

func newConnPool(
	opt *Options,
	dialer func(ctx context.Context, network, addr string) (net.Conn, error),
) *pool.ConnPool {
	return pool.NewConnPool(&pool.Options{
		Dialer: func(ctx context.Context) (net.Conn, error) {
			return dialer(ctx, opt.Network, opt.Addr)
		},
		PoolFIFO:        opt.PoolFIFO,
		PoolSize:        opt.PoolSize,
		PoolTimeout:     opt.PoolTimeout,
		MinIdleConns:    opt.MinIdleConns,
		MaxIdleConns:    opt.MaxIdleConns,
		MaxActiveConns:  opt.MaxActiveConns,
		ConnMaxIdleTime: opt.ConnMaxIdleTime,
		ConnMaxLifetime: opt.ConnMaxLifetime,
	})
}
